// Copyright 2005 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.portlet;

import java.util.BitSet;
import java.util.regex.Pattern;

import javax.portlet.PortletConfig;
import javax.portlet.PortletContext;

import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.internal.TapestryAppInitializer;
import org.apache.tapestry5.internal.portlet.PortletContextSymbolProvider;
import org.apache.tapestry5.ioc.Registry;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.services.SymbolProvider;
import org.apache.tapestry5.portlet.services.PortletApplicationInitializer;
import org.apache.tapestry5.portlet.services.PortletModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Holds the singleton registry that exists between each portlet in the application.
 * Provides initialization and access to that registry
 */
public final class PortletUtilities {

    private static final Logger _logger   = LoggerFactory.getLogger(PortletUtilities.class);

    private static Registry     _registry = null;

    private PortletUtilities() {

    }

    // Since multiple portlets share the registry and additionally need to access it to define action
    // and render handlers, this method makes the singleton registry available to the portlets.  This was
    // done to prevent initialization of multiple registries, making the application extremely slow.
    public static synchronized Registry getRegistry(PortletConfig portletConfig) {
        if (PortletUtilities._registry == null) {
            initRegistry(portletConfig);
        }
        return PortletUtilities._registry;
    }

    private static void initRegistry(PortletConfig portletConfig) {
        PortletContext context = portletConfig.getPortletContext();

        SymbolProvider provider = new PortletContextSymbolProvider(context);

        String appName = context.getPortletContextName();

        String executionMode = System.getProperty("tapestry.execution-mode", "production");

        TapestryAppInitializer appInitializer = new TapestryAppInitializer(PortletUtilities._logger, provider, appName,
            "portlet", executionMode);

        // Since this is a portlet page, this module refits some of the pages for portlets
        appInitializer.addModules(PortletModule.class);

        addAppModule(appInitializer, provider);

        PortletUtilities._registry = appInitializer.createRegistry();

        //        long start = appInitializer.getStartTime();
        //
        //        long toRegistry = appInitializer.getRegistryCreatedTime();

        PortletApplicationInitializer ai = PortletUtilities._registry.getService("PortletApplicationInitializer",
            PortletApplicationInitializer.class);
        //
        ai.initializeApplication(portletConfig.getPortletContext());

        PortletUtilities._registry.performRegistryStartup();
        //
        //        long toFinish = System.currentTimeMillis();
        //
        //        _logger.info(format("Startup time: %,d ms to build IoC Registry, %,d ms overall.", toRegistry
        //                - start, toFinish - start));

        appInitializer.announceStartup();

    }

    private static void addAppModule(TapestryAppInitializer appInitializer, SymbolProvider provider) {
        String appPackage = provider.valueForSymbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM);
        String className = appPackage + ".services." + "App" + "Module";

        try {
            // This class is possibly loaded by a parent class loader of the application class
            // loader. The context class loader should have the appropriate view to the module class,
            // if any.

            Class moduleClass = Thread.currentThread().getContextClassLoader().loadClass(className);

            appInitializer.addModules(moduleClass);
        }
        catch (ClassNotFoundException ex) {
            // That's OK, not all applications will have a module class, even though any
            // non-trivial application will.
        }
    }

    private static final URLCodec CODEC = new URLCodec() {

                                            private BitSet contextSafe = (BitSet) URLCodec.WWW_FORM_URL.clone();

                                            {
                                                // Servlet container does not decode '+' in path to ' ',
                                                // so we encode ' ' to %20, not to '+'.
                                                this.contextSafe.clear(' ');
                                            }

                                            @Override
                                            public byte[] encode(byte[] bytes) {
                                                return encodeUrl(this.contextSafe, bytes);
                                            }
                                        };

    /**
     * Encodes a string for inclusion in a URL.  Slashes and percents are converted to "%25" and "%2F" respectively,
     * then the entire string is  URL encoded.
     *
     * @param input string to include, may not be blank
     * @return encoded input
     */
    public static String encodeContext(String input) {
        assert InternalUtils.isNonBlank("input");

        try {
            return PortletUtilities.CODEC.encode(escapePercentAndSlash(input));
        }
        catch (EncoderException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static final String  PERCENT                 = "%";

    private static final Pattern PERCENT_PATTERN         = Pattern.compile(PortletUtilities.PERCENT);

    private static final String  ENCODED_PERCENT         = "%25";

    private static final Pattern ENCODED_PERCENT_PATTERN = Pattern.compile(PortletUtilities.ENCODED_PERCENT);

    private static final String  SLASH                   = "/";

    private static final Pattern SLASH_PATTERN           = Pattern.compile(PortletUtilities.SLASH);

    private static final String  ENCODED_SLASH           = "%2F";

    private static final Pattern ENCODED_SLASH_PATTERN   = Pattern.compile(PortletUtilities.ENCODED_SLASH,
                                                             Pattern.CASE_INSENSITIVE);

    /**
     * Encodes percent and slash characters in the string for later decoding via {@link
     * #unescapePercentAndSlash(String)}.
     *
     * @param input string to encode
     * @return modified string
     */
    public static String escapePercentAndSlash(String input) {
        return replace(replace(input, PortletUtilities.PERCENT_PATTERN, PortletUtilities.ENCODED_PERCENT),
            PortletUtilities.SLASH_PATTERN, PortletUtilities.ENCODED_SLASH);
    }

    /**
     * Used to decode certain escaped characters that are replaced when using {@link #encodeContext(String)}}.
     *
     * @param input a previously encoded string
     * @return the string with slash and percent characters restored
     */
    public static String unescapePercentAndSlash(String input) {
        return replace(replace(input, PortletUtilities.ENCODED_SLASH_PATTERN, PortletUtilities.SLASH),
            PortletUtilities.ENCODED_PERCENT_PATTERN, PortletUtilities.PERCENT);
    }

    private static String replace(String input, Pattern pattern, String replacement) {
        return pattern.matcher(input).replaceAll(replacement);
    }

}
